<?php
// +----------------------------------------------------------------------
// | ThinkCrypto
// +----------------------------------------------------------------------
// | 版权所有 2014~2021 青海云音信息技术有限公司 [ http://www.yyinfos.com ]
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// +----------------------------------------------------------------------
// | gitcode 代码仓库：https://gitcode.net/qhweb/ThinkCrypto
// +----------------------------------------------------------------------

declare(strict_types=1);

namespace think\crypto\libs;

use RuntimeException;

/**
 * 安全加密扩展（OpenSSL）
 * 支持：AES-CBC/CFB/CTR/ECB，Camellia-CBC/CFB，以及 AEAD 模式（GCM/CCM/OCB，若可用）
 */
class AES
{
    // 算法与模式
    private const CIPHER_AES = 'aes-256-%s';
    private const CIPHER_CAMELLIA = 'camellia-256-%s';

    // 支持的加密模式（键为模式名，值为所需参数配置）
    private const MODES = [
        'cbc' => ['iv' => 16, 'needs_tag' => false],
        'cfb' => ['iv' => 16, 'needs_tag' => false],
        'ctr' => ['iv' => 16, 'needs_tag' => false],
        'ecb' => ['iv' => 0,  'needs_tag' => false],
        'gcm' => ['iv' => 12, 'needs_tag' => true,  'tag_len' => 16],
        'ccm' => ['iv' => 12, 'needs_tag' => true,  'tag_len' => 16],
        'ocb' => ['iv' => 12, 'needs_tag' => true,  'tag_len' => 16],
    ];

    private const HASH_ALGO = 'sha3-256';
    private const KDF_ALGO = 'sha3-512';
    private const KDF_SALT_AES = 'yycms-aes';
    private const KDF_SALT_CAMELLIA = 'yycms-camellia';
    private const KDF_ITER = 5000; // AES派生迭代次数（Camellia使用KDF_ITER/2）

    /** @var string 32字节二进制主密钥 */
    private string $secret;

    /** @var array 派生密钥 [AES密钥(32B), Camellia密钥(32B)] */
    private array $keys;

    /** @var bool 是否支持 Camellia */
    private bool $supportCamellia;

    /** @var bool 是否支持 OCB（运行时探测） */
    private bool $supportOcb;

    /** @var string 返回数据类型 */
    private string $format;

    public function __construct(string $secret = '',string $format = 'hex')
    {
        if (!extension_loaded('openssl')) {
            throw new RuntimeException('OpenSSL扩展未启用');
        }

        // 生成或标准化主密钥为32字节二进制
        $this->secret = $secret === '' ? $this->generateRandomKey(32) : hash(self::HASH_ALGO, $secret, true);
        if (strlen($this->secret) !== 32) {
            throw new RuntimeException('密钥长度必须为32字节（256位）');
        }

        $this->supportCamellia = in_array(strtolower(self::CIPHER_CAMELLIA), openssl_get_cipher_methods(), true);
        $this->supportOcb = in_array('aes-128-ocb', openssl_get_cipher_methods(), true);
        $this->format = $format;

        $this->initKeys();
    }

    private function generateRandomKey(int $len): string
    {
        return random_bytes($len);
    }

    private function initKeys(): void
    {
        $kdf = fn(string $salt, int $iter): string => substr(
            hash_pbkdf2(self::KDF_ALGO, $this->secret, $salt, $iter, 64, true),
            0, 32
        );
        $this->keys = [
            $kdf(self::KDF_SALT_AES, self::KDF_ITER),
            $kdf(self::KDF_SALT_CAMELLIA, (int)(self::KDF_ITER / 2)),
        ];
    }

    // ================= 公共接口 =================

    /**
     * 指定模式加密（统一入口）
     *
     * @param string $data 明文
     * @param string $mode 模式：cbc|cfb|ctr|ecb|gcm|ccm|ocb
     * @param string $aad  附加认证数据（仅GCM/CCM/OCB有效）
     * @return string 编码后的密文
     * @throws RuntimeException
     */
    public function encryptWithMode(string $data, string $mode, string $aad = ''): string
    {
        $cfg = $this->modeConfig($mode);
        $iv = $cfg['iv'] > 0 ? random_bytes($cfg['iv']) : '';

        $cipher = $this->cipher(self::CIPHER_AES, $mode);
        $key = $this->keys[0];

        $options = OPENSSL_RAW_DATA;
        $tag = '';

        if ($cfg['needs_tag']) {
            $tagLen = $cfg['tag_len'];
            $ciphertext = openssl_encrypt($data, $cipher, $key, $options, $iv, $tag, $aad, $tagLen);
            if ($ciphertext === false) {
                throw new RuntimeException('加密失败：' . openssl_error_string());
            }
            // 输出格式：IV(12B) || 密文 || TAG(16B)
            return base64_encode($iv . $ciphertext . $tag);
        }

        // 非AEAD：先做完整性校验，再加密
        $mac = hash('crc32c', $data, true); // 4B
        $payload = $data . $mac;
        $ciphertext = openssl_encrypt($payload, $cipher, $key, $options, $iv);
        if ($ciphertext === false) {
            throw new RuntimeException('加密失败：' . openssl_error_string());
        }
        // 输出格式：IV(16B) || 密文
        return $this->format == 'hex' ? bin2hex($iv . $ciphertext) : base64_encode($iv . $ciphertext);
    }

    /**
     * 指定模式解密（统一入口）
     *
     * @param string $data 密文（Base64）
     * @param string $mode 模式：cbc|cfb|ctr|ecb|gcm|ccm|ocb
     * @param string $aad  附加认证数据（仅GCM/CCM/OCB有效）
     * @return string 明文
     * @throws RuntimeException
     */
    public function decryptWithMode(string $data, string $mode, string $aad = ''): string
    {
        $cfg = $this->modeConfig($mode);
        $data = $this->format == 'hex' ? hex2bin($data) : base64_decode($data, true);
        if ($data === false) {
            throw new RuntimeException('密文Base64解码失败');
        }

        $cipher = $this->cipher(self::CIPHER_AES, $mode);
        $key = $this->keys[0];

        $options = OPENSSL_RAW_DATA;
        $tag = '';

        if ($cfg['needs_tag']) {
            $tagLen = $cfg['tag_len'];
            $iv = substr($data, 0, $cfg['iv']);
            $tag = substr($data, -$tagLen);
            $ciphertext = substr($data, $cfg['iv'], -$tagLen);
            $plain = openssl_decrypt($ciphertext, $cipher, $key, $options, $iv, $tag, $aad);
            if ($plain === false) {
                throw new RuntimeException('解密/验签失败：' . openssl_error_string());
            }
            return $plain;
        }

        // 非AEAD：解密后校验CRC32C
        $iv = substr($data, 0, $cfg['iv']);
        $payload = openssl_decrypt(substr($data, $cfg['iv']), $cipher, $key, $options, $iv);
        if ($payload === false) {
            throw new RuntimeException('解密失败：' . openssl_error_string());
        }
        if (strlen($payload) < 4) {
            throw new RuntimeException('密文过短，数据损坏');
        }
        $mac = substr($payload, -4);
        $text = substr($payload, 0, -4);
        if (!hash_equals($mac, hash('crc32c', $text, true))) {
            throw new RuntimeException('数据完整性校验失败');
        }
        return $text;
    }

    /**
     * 兼容旧接口：AES(CBC)+Camellia(CFB)双层 + 压缩 + CRC32C + Base64 + str_rot13 + gzcompress(9)
     */
    public function encrypt(string $data): string
    {
        $data = $data . hash('crc32c', $data, true); // 4B
        $data = $this->aesEncrypt($data);            // CBC
        $data = $this->camelliaEncrypt($data);       // CFB
        return base64_encode(str_rot13(gzcompress($data, 9)));
    }

    /**
     * 兼容旧接口：逆序解密
     */
    public function decrypt(string $data): string
    {
        $data = gzuncompress(str_rot13(base64_decode($data)));
        $data = $this->camelliaDecrypt($data);
        $data = $this->aesDecrypt($data);
        if (strlen($data) < 4) {
            throw new RuntimeException('密文过短，数据损坏');
        }
        $mac = substr($data, -4);
        $text = substr($data, 0, -4);
        if (!hash_equals($mac, hash('crc32c', $text, true))) {
            throw new RuntimeException('数据完整性校验失败');
        }
        return $text;
    }

    // ================= 内部实现 =================

    private function modeConfig(string $mode): array
    {
        $m = strtolower($mode);
        if (!isset(self::MODES[$m])) {
            throw new RuntimeException("不支持的加密模式：{$mode}");
        }
        if ($m === 'ecb') {
            trigger_error('ECB模式不安全，仅用于兼容旧系统，不建议在生产环境使用', E_USER_WARNING);
        }
        if ($m === 'ocb' && !$this->supportOcb) {
            throw new RuntimeException('当前环境不支持OCB模式');
        }
        return self::MODES[$m];
    }

    private function cipher(string $family, string $mode): string
    {
        $algo = sprintf($family, $mode);
        if (!in_array(strtolower($algo), openssl_get_cipher_methods(), true)) {
            throw new RuntimeException("算法不可用：{$algo}");
        }
        return $algo;
    }

    // --- 原有双层加解密（保持兼容） ---
    private function aesEncrypt(string $data): string
    {
        $iv = random_bytes(16);
        $cipher = $this->cipher(self::CIPHER_AES, 'cbc');
        return $iv . openssl_encrypt($data, $cipher, $this->keys[0], OPENSSL_RAW_DATA, $iv);
    }

    private function aesDecrypt(string $data): string
    {
        $iv = substr($data, 0, 16);
        $cipher = $this->cipher(self::CIPHER_AES, 'cbc');
        return openssl_decrypt(substr($data, 16), $cipher, $this->keys[0], OPENSSL_RAW_DATA, $iv);
    }

    private function camelliaEncrypt(string $data): string
    {
        if (!$this->supportCamellia) {
            throw new RuntimeException('当前环境不支持Camellia');
        }
        $iv = random_bytes(16);
        $cipher = $this->cipher(self::CIPHER_CAMELLIA, 'cfb');
        return $iv . openssl_encrypt($data, $cipher, $this->keys[1], OPENSSL_RAW_DATA, $iv);
    }

    private function camelliaDecrypt(string $data): string
    {
        if (!$this->supportCamellia) {
            throw new RuntimeException('当前环境不支持Camellia');
        }
        $iv = substr($data, 0, 16);
        $cipher = $this->cipher(self::CIPHER_CAMELLIA, 'cfb');
        return openssl_decrypt(substr($data, 16), $cipher, $this->keys[1], OPENSSL_RAW_DATA, $iv);
    }

    public function __destruct()
    {
        // 安全清理
        $this->keys = array_map(fn($k) => str_repeat("\0", strlen($k)), $this->keys);
        $this->secret = str_repeat("\0", strlen($this->secret));
    }
}